/*
 * ﻿Copyright (C) 2013 NewMain Softech
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
 * either express or implied. See the License for the specific language
 * governing permissions and limitations under the License.
 */
package com.newmainsoftech.eatestutil.container.glassfish;

import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.text.MessageFormat;
import java.util.Random;

import org.apache.derby.jdbc.EmbeddedDriver;
import org.codehaus.cargo.container.ContainerType;
import org.codehaus.cargo.container.InstalledLocalContainer;
import org.codehaus.cargo.container.configuration.ConfigurationType;
import org.codehaus.cargo.container.configuration.LocalConfiguration;
import org.codehaus.cargo.container.configuration.entry.DataSource;
import org.codehaus.cargo.container.glassfish.GlassFishPropertySet;
import org.codehaus.cargo.container.installer.ZipURLInstaller;
import org.codehaus.cargo.container.property.GeneralPropertySet;
import org.codehaus.cargo.container.property.ServletPropertySet;
import org.codehaus.cargo.generic.DefaultContainerFactory;
import org.codehaus.cargo.generic.configuration.DefaultConfigurationFactory;
import org.codehaus.cargo.util.log.FileLogger;
import org.codehaus.cargo.util.log.LogLevel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.newmainsoftech.eatestutil.container.ContainerConfig;
import com.newmainsoftech.eatestutil.container.InstalledLocalContainerWrapper;

/**
 * Helper class for setting up Glassfish.<br /> 
 * 
 * @author Arata Y.
 */
public class GlassfishConfig implements ContainerConfig {
	Logger logger = LoggerFactory.getLogger( this.getClass());
		protected Logger getLogger() {
			return logger;
		}
	
	public static final String DefaultCcontainerInstallerUrlStr 
	= "http://download.java.net/glassfish/4.0/release/glassfish-4.0.zip";
	private String containerInstallerUrlStr = EmbeddedGlassfishConfig.DefaultCcontainerInstallerUrlStr;
		public String getContainerInstallerUrlStr() {
			return containerInstallerUrlStr;
		}
		/**
		 * {@inheritDoc}
		 * Default to {@value #DefaultCcontainerInstallerUrlStr}. 
		 * If it is necessary to change to other URL, then this setter method needs to be called 
		 * before invocation of <code>{@link #prepInstalledLocalContainer()}<code> method.
		 */
		public void setContainerInstallerUrlStr( final String containerInstallerUrlStr) {
			this.containerInstallerUrlStr = containerInstallerUrlStr;
		}
	public final static String DefaultContainerId = "gfByGfConfig";
	private String containerId = GlassfishConfig.DefaultContainerId;
		public String getContainerId() {
			return containerId;
		}
		/**
		 * {@inheritDoc}
		 * Default to {@value #DefaultContainerId}. If it is necessary to change to other value than 
		 * default value, then this method needs to be called before invocation of 
		 * <code>{@link #prepInstalledLocalContainer()}</code> method.
		 */
		public void setContainerId( final String containerId) {
			this.containerId = containerId;
		}
	private ContainerType typeOfContainer = ContainerType.INSTALLED;
		public ContainerType getTypeOfContainer() {
			return typeOfContainer;
		}
		/**
		 * {@inheritDoc}
		 * Default to {@link ContainerType#INSTALLED}. If it is necessary to change to other than 
		 * default, then this method must be called before invocation of 
		 * <code>{@link #prepInstalledLocalContainer()}</code> method.
		 */
		public void setTypeOfContainer( final ContainerType typeOfContainer) {
			this.typeOfContainer = typeOfContainer;
		}
	private ConfigurationType configurationType = ConfigurationType.STANDALONE;
		public ConfigurationType getConfigurationType() {
			return configurationType;
		}
		/**
		 * {@inheritDoc}
		 * Default to <code>{@link ConfigurationType#STANDALONE}</code>. If it is necessary 
		 * to change to other than default, then this method must be called before invocation 
		 * of code>{@link #prepInstalledLocalContainer()}</code> method.
		 */
		public void setConfigurationType( final ConfigurationType configurationType) {
			this.configurationType = configurationType;
		}
	public final static String DefaultTestDBName = "testDB";
	private String testDBName = GlassfishConfig.DefaultTestDBName;
		public String getTestDBName() {
			return testDBName;
		}
		/**
		 * {@inheritDoc}
		 * Default to {@value #DefaultTestDBName}. If it is necessary to change to other 
		 * value than default, then this method needs to be called before invocation of 
		 * <code>{@link #prepInstalledLocalContainer()}</code> method.
		 */
		public void setTestDBName( final String testDBName) {
			this.testDBName = testDBName;
		}
	private String dbTestUserName;
		public String getDbTestUserName() {
			return dbTestUserName;
		}
		/**
		 * {@inheritDoc}
		 * Default to random hex integer value. If it is necessary to change to other 
		 * than default value, then this method needs to be called before invocation 
		 * of <code>{@link #prepInstalledLocalContainer()}</code> method.
		 */
		public void setDbTestUserName( final String dbTestUserName) {
			this.dbTestUserName = dbTestUserName;
		}
	private String dbTestUserPassword;
		public String getDbTestUserPassword() {
			return dbTestUserPassword;
		}
		/**
		 * {@inheritDoc}
		 * Default to random hex integer value. If it is necessary to change to 
		 * other than default value, then this method needs to be called before 
		 * invocation of <code>{@link #prepInstalledLocalContainer()}</code> method.
		 */
		public void setDbTestUserPassword( final String dbTestUserPassword) {
			this.dbTestUserPassword = dbTestUserPassword;
		}
	public final static String DefaultDomainName = "ejb_domain";
	private String domainName = EmbeddedGlassfishConfig.DefaultDomainName;
		public String getDomainName() {
			return domainName;
		}
		/**
		 * {@inheritDoc}
		 * Default to {@value #DefaultDomainName}. If it is necessary to change to other 
		 * than default value, then this method needs to be called before invocation of 
		 * <code>{@link #prepInstalledLocalContainer()}</code> method.
		 */
		public void setDomainName( final String domainName) {
			this.domainName = domainName;
		}
	private String configHomeDirPath;
		public String getConfigHomeDirPath() {
			return configHomeDirPath;
		}
		/**
		 * {@inheritDoc}
		 * Default to target/&lt;container id&gt;_config directory of what &lt;container id&gt; part 
		 * will be replaced with value by <code>{@link #getContainerId()}</code> method. If it is 
		 * necessary to change to other than default, then this method must be called before invocation 
		 * of  <code>{@link #prepInstalledLocalContainer()}</code> method.
		 */
		public void setConfigHomeDirPath( final String configHomeDirPath) {
			this.configHomeDirPath = configHomeDirPath;
		}
	
	private LocalConfiguration localConfiguration;
		LocalConfiguration getLocalConfiguration() {
			return localConfiguration;
		}
		void setLocalConfiguration( final LocalConfiguration localConfiguration) {
			this.localConfiguration = localConfiguration;
		}
	/* Yields <code>LocalConfiguration</code> object. <br />
	 * <code>LocalConfiguration</code> object is yielded from return values of following methods:
	 * <ul>
	 *  <li>{@link #getContainerId()}</li>
	 *  <li>{@link #getTypeOfContainer()}</li>
	 *  <li>{@link #getConfigurationType()}</li>
	 *  <li>{@link #getConfigHomeDirPath()}</li>
	 *  <li>{@link #getTestDBName()}</li>
	 * </ul>
	 * Yielded <code>LocalConfiguration</code> object will also contain datasource configuration 
	 * with following settings:
	 * <ul>
	 *  <li>Derby database in memory will be created.</li>
	 *  <li>Return value of <code>{@link #getTestDBName()}</code> method will be used for 
	 *  	name of schema.
	 *  </li>
	 *  <li>Datasource driver class will be <code>org.apache.derby.jdbc.EmbeddedDriver</code>.</li>
	 *  <li>JNDI location will be jdbc/&lt;database name&gt; of what &lt;database name&gt; part will 
	 *  	be replaced by return value of <code>{@link #getTestDBName()}</code> method.
	 *  </li>
	 *  <li>Return value of <code>{@link #getDbTestUserName()}</code> method will be used for 
	 *  	user name to create/access database.
	 *  </li>
	 *  <li>Return value of <code>{@link #getDbTestUserPassword()}</code> method will be used for 
	 *  	user password to create/access database.
	 *  </li>
	 *  <li>Return value of <code>{@link #getDomainName()}</code> method will be used for domain name.
	 *  </li>
	 *  <li>Use port 8080.</li>
	 * </ul> 
	 */
	LocalConfiguration prepLocalConfiguration() {
		// Create container configuration ---------------------------------------------------------
		final DefaultConfigurationFactory defaultConfigurationFactory 
		= new DefaultConfigurationFactory();
		
		final LocalConfiguration localConfiguration 
		= (LocalConfiguration)(defaultConfigurationFactory
				.createConfiguration( 
						getContainerId(), 
						getTypeOfContainer(), 
						getConfigurationType(), 
						getConfigHomeDirPath())
				);
			// Add datasource for derby embedded to glassfish
			final DataSource dataSource = new DataSource();
				dataSource.setId( getTestDBName());
				dataSource.setDriverClass( EmbeddedDriver.class.getName());
				dataSource.setConnectionType( "java.sql.Driver");
					// According to http://cargo.codehaus.org/DataSource+and+Resource+Support, 
					// connection type should not be explicitly set, but, without setting it,  
					// value of restype parameter handing over to glassfish's admin-cli.jar became 
					// null and it fails by invalid parameter for resType option. 
				dataSource.setUrl( 
						"jdbc:derby:memory:".concat( getTestDBName()).concat( ";")
	//							.concat( "user=").concat( getDbTestUserName()).concat( ";")
	//							.concat( "password=").concat( getDbTestUserPassword()).concat( ";")
						.concat( "create=true;"));
					// Glassfish's bundled Derby DB allows for anyone to create table, and 
					// allows for anyone to access table as long as even one only knows user 
					// name but password. 
					// For further info, see http://www.ensode.net/roller/dheffelfinger/entry/apache_derby_insecure_by_default
				dataSource.setJndiLocation( "jdbc/".concat( getTestDBName()));
				
				Random random = new Random();
				String userName = getDbTestUserName();
					if ( userName == null) {
						userName = Integer.toHexString( random.nextInt());
						setDbTestUserName( userName);
					}
					dataSource.setUsername( userName);
				String userPassword = getDbTestUserPassword();
					if ( userPassword == null) {
						userPassword = Integer.toHexString( random.nextInt());
						setDbTestUserPassword( userPassword);
					}
					dataSource.setPassword( userPassword);
			localConfiguration.addDataSource( dataSource);
			
			localConfiguration.setProperty( 
					GlassFishPropertySet.DOMAIN_NAME, getDomainName());
			localConfiguration.setProperty( 
					GlassFishPropertySet.DEBUG_MODE, "true");
			localConfiguration.setProperty(
					ServletPropertySet.PORT, "8080");
			localConfiguration.setProperty(
					GeneralPropertySet.LOGGING, "high");
		// ----------------------------------------------------------------------------------------
		return localConfiguration;
	}
	
	private String extractDirPath;
		public String getExtractDirPath() {
			return extractDirPath;
		}
		/**
		 * {@inheritDoc}
		 * Default to target/&lt;container id&gt;_config directory of what &lt;container id&gt; part 
		 * will be replaced with value by <code>{@link #getContainerId()}</code> method. If it is 
		 * necessary to change to other than default, then this method must be called before invocation 
		 * of  <code>{@link #prepInstalledLocalContainer()}</code> method.
		 */
		public void setExtractDirPath( final String extractDirPath) {
			this.extractDirPath = extractDirPath;
		}
	private File cargoLogFile;
		public File getCargoLogFile() {
			return cargoLogFile;
		}
		public void setCargoLogFile( final File cargoLogFile) {
			this.cargoLogFile = cargoLogFile;
		}
		
	// Constructors -------------------------------------------------------------------------------
	public GlassfishConfig( final String extractDirPath) {
		File extractDir;
			if ( extractDirPath == null) {
				extractDir = new File( ".", "java_ee_container");
			}
			else {
				extractDir = new File( extractDirPath);
			}
			
		File configHomeDir 
		= new File( ".", "target".concat( File.separator)
				.concat( getContainerId()).concat( "_config"));
		try {
			setExtractDirPath( extractDir.getCanonicalPath());
		}
		catch( IOException exception) {
			throw new RuntimeException( exception);
		}
	}
	
	public GlassfishConfig( 
			final String extractDirPath, final String configHomeDirPath) {
		this( extractDirPath);
		
		File configHomeDir;
			if ( configHomeDirPath == null) {
				configHomeDir
				= new File( ".", "target".concat( File.separator)
						.concat( getContainerId()).concat( "_config"));
			}
			else {
				configHomeDir = new File( configHomeDirPath);
			}
		try {
			setConfigHomeDirPath( configHomeDir.getCanonicalPath());
		}
		catch( IOException exception) {
			throw new RuntimeException( exception);
		}
		setCargoLogFile( 
				new File( configHomeDir, "cargo.log"));
	}
	public GlassfishConfig() {
		this( null, null);
	}
	// --------------------------------------------------------------------------------------------
	
	public final static long DefaultContainerStartTimeout = 12 * 60 * 1000; // [ms]
	private long containerStartTimeout = EmbeddedGlassfishConfig.DefaultContainerStartTimeout; // [ms]
		public long getContainerStartTimeout() {
			return containerStartTimeout;
		}
		/**
		 * {@inheritDoc}
		 * Default to {@value #DefaultContainerStartTimeout}}[ms]. If it is necessary to change 
		 * to other than default, then this method needs to be called before invocation of 
		 * <code>{@link #prepInstalledLocalContainer()}</code> method.
		 */
		public void setContainerStartTimeout( long containerStartTimeout) {
			this.containerStartTimeout = containerStartTimeout;
		}
	/**
	 * {@inheritDoc}
	 * Glassfish is installed from URL pointed with <code>{@link #getContainerInstallerUrlStr()}</code> 
	 * method to where the return value of <code>{@link #getExtractDirPath()}</code> method points.
	 */
	public InstalledLocalContainerWrapper prepInstalledLocalContainer() throws Exception {
		InstalledLocalContainerWrapper installedLocalContainerWrapper
		= new InstalledLocalContainerWrapper();
		
		// install the container from a URL pointing to its distribution --------------------------
		final URL containerInstallerUrl = new URL( getContainerInstallerUrlStr());
		final ZipURLInstaller installer = new ZipURLInstaller( containerInstallerUrl);
				installer.setExtractDir( getExtractDirPath());
				if ( getLogger().isDebugEnabled()) {
					getLogger().debug( 
							MessageFormat.format( 
									"Downloading {0} from {1} to {2}, and installing to {3}.", 
									getContainerId(), 
									getContainerInstallerUrlStr(),
									installer.getDownloadDir(),
									getExtractDirPath())
							);
				}
			installer.getLogger().setLevel( LogLevel.DEBUG);
			installer.install();
				if ( getLogger().isDebugEnabled()) {
					getLogger().debug( 
							String.format(
									"%1$s was installed to %2$s.",
									getContainerId(), 
									installer.getHome())
							);
				}
		installedLocalContainerWrapper.setZipURLInstaller( installer);
		// ----------------------------------------------------------------------------------------
		
		// Create Cargo Container instance wrapping our physical container and start container ----
		final LocalConfiguration localConfiguration = prepLocalConfiguration();
		
		final DefaultContainerFactory defaultContainerFactory = new DefaultContainerFactory();
		final InstalledLocalContainer installedLocalContainer 
		= (InstalledLocalContainer)(defaultContainerFactory.createContainer( 
				getContainerId(), 
				getTypeOfContainer(), 
				localConfiguration));
			installedLocalContainer.setHome( installer.getHome());
			installedLocalContainer.setTimeout( getContainerStartTimeout());
			final FileLogger cargoLogger = new FileLogger( getCargoLogFile(), false);
				cargoLogger.setLevel( LogLevel.DEBUG);
			installedLocalContainer.setLogger( cargoLogger);
			
	/*				final File projectClassesDir 
				= new File( ".", "target".concat( File.separator).concat( "classes"));
					installedLocalContainer.addExtraClasspath( projectClassesDir.getCanonicalPath());
				final File projectTestClassesDir
				= new File( ".", "target".concat( File.separator).concat( "test-classes"));
					installedLocalContainer.addExtraClasspath( projectTestClassesDir.getCanonicalPath());
	*/
		installedLocalContainerWrapper.setInstalledLocalContainer( installedLocalContainer);
		// ----------------------------------------------------------------------------------------
			if ( getLogger().isDebugEnabled()) {
				getLogger().debug( 
						String.format(
								"Cargo wrapping %1$s is ready to start.",
								getContainerId())
						);
			}
		return installedLocalContainerWrapper;
	}
}
